閉包,是一個 JavaScript 很常聽到的觀念,雖然會在不知不覺應用到閉包的基本概念,進階的用法在實戰中不一定很常用到,但這卻是面試中非常常見的問題,主要原因是它牽扯到許多的觀念,如詞法作用域、記憶體等觀念。
許多閉包文章都會提到「詞法作用域」的觀念,此觀念主要是說明 JavaScript 的作用域範圍由程式碼所定,所以程式碼在撰寫完成的同時,就已經先確立了作用域,運行的過程中都不會改變其作用域,範例程式碼如下:
function fn1() {
console.log(a);
}
function fn2() {
var a = 1;
fn1();
}
fn2();
上方兩個函式的作用域完全獨立,不會因為運行的過程讓函式可存取領一個函式的變數。
雖然變數的作用域是獨立的,但函式內的函式則可以取用外層作用域的變數。
如以下範例:addString
函式內並沒有 name
的變數,因此他會從外層尋找,在外層的 sayHi
函式則可以找到 name
變數。
function sayHi() {
var name = '小明';
function addString() { // 內部函式、閉包
console.log(`${name} 你好`); // 取用外層的變數
}
addString();
}
sayHi();
在巢狀函式中,如果內層的函式沒有可以取用的特定變數則會向外查找,此時內部的函式就可以稱為閉包。
內層函式取用外層函式的變數,就這樣而已?當然不止,這個概念可以有非常多延伸的變化。
前幾篇文章有提到「記憶體釋放」的觀念,當函式中的變數無法再被參考時,該變數所佔用的記憶體就會被釋放掉,而閉包的技巧正好可以維持記憶體的參考。
可參考以下範例,此範例中(可搭配下方的圖片一起參考):
sayHi
函式中傳入 name 的參數,此參數的作用域在 sayHi 函式中,當此變數無法再次被存取就會被釋放。addString
存取 name
變數,由於該函式內沒有此變數因此向外層查找到 sayHi 函式的 name
變數addString
沒有在 sayHi
函式內運行,而是透過 return 傳出到 mingSayHi 的變數上(小明說你好)。mingSayHi
時:
addString
函式,並且嘗試存取 name
變數...name
變數因為還會持續維持參考,所以不會被釋放記憶體,因此成為了 mingSayHi
函式的私有變數(僅存與此函式中,無法透過其它方式調整)function sayHi(name) {
function addString() {
console.log(`${name} 你好`)
}
return addString;
}
var mingSayHi = sayHi('小明');
mingSayHi();
圖解概念:
mingSayHi
上mingSayHi
是一個函式變數,當運行時可以存取 sayHi 的變數,此時的 name
變數為 mingSayHi
的私有變數。閉包是內部的函式可以取用外部作用域變數的概念,單就此概念來說僅是函式及詞法作用域的關係,但如果將內層的函式向外傳出,使其保留了原始函式結構中的作用域使其產生私有變數,更能凸顯閉包的價值。
在這個結構中出現了兩層函式,第一次呼叫時會運行 sayHi
函式,第二次呼叫則會運行內部回傳的 addString
函式;除了上述介紹私有變數時將回傳函式儲存於 mingSayHi
外,也可直接使用兩個括號來執行內部的函式。
sayHi('杰倫')(); // 杰倫 你好
呼叫流程如下
('杰倫')
:運行 sayHi
,並傳入 '杰倫'()
:運行 addString
,直接回傳 '杰倫 你好'模擬事件:用戶有一個錢包存有 1000 元,這個錢包會因為不斷的購買品項而減少費用,每次所減少的費用都不大相同。
下方建立了包含閉包特性的函式:
function buyItem() {
var myMoney = 1000;
return function (price) { // 這個閉包目前會被重複呼叫
myMoney = myMoney - price;
// myMoney 第一次由外部傳入,接下來在這個 function 內不斷更新
return myMoney;
}
}
var mingMoney = buyItem(); // 存取內部函式的變數
mingMoney(100); // 900
mingMoney(100); // 800
mingMoney(100); // 700
模擬事件更新:用戶不僅一名,會有另外一名共同使用此方法,但兩者的金錢不能混合計算。
在上方所建立包含私有變數的函式中就可以解決此問題,可使用另一個變數來建立一個新函式,兩個函式會擁有各自的私有變數。
var jayMoney = buyItem(); // 存取內部函式的變數
jayMoney(50); // 950
jayMoney(100); // 850
jayMoney(500); // 350
mingMoney(100); // 600,mingMoney 與上方的 jayMoney 變數不會共用
除了單一的方法匯出以外,閉包另有私有方法(可參考 MDN 文件)。
閉包,除了是面試常見的考題外,實戰中也有私有變數的優點(函式的作用域內,外部無法取得函式內的變數)。因此,無論匯出的是哪一種方法,閉包的優點是變數僅存在於函式之中,如果匯出的方法沒有提供原始值,將無法用任何方式取得原始變數,如果需要刻意隱藏變數值,避免用戶透過其他工具或方式取得,閉包也會是一個好方法。